package com.nulldev.util.web.HttpClient.clients.impld;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.CookieHandler;
import java.net.URI;
import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.JVM.reflections.ClassCache;
import com.nulldev.util.JVM.reflections.Reflections;
import com.nulldev.util.concurrency.threadIt.v4.emapi.cntft.ControllableFutureTask;
import com.nulldev.util.concurrency.threadIt.v4.emapi.uft.Executable;
import com.nulldev.util.data.Arrays.maps.EmptyMap;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.web.HttpClient.HttpMethod;
import com.nulldev.util.web.HttpClient.HttpRequest;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpClient.ResponseFormat;
import com.nulldev.util.web.HttpClient.contentGen.GeneratableContent;
import com.nulldev.util.web.HttpClient.util.HttpStreamUtil;
import com.nulldev.util.web.URLUtils.Entry;

public abstract class NativeHttpRequest<T> implements HttpRequest<T> {
	private final static Object client;

	static {
		if (JVM.version() >= 11f) {
			final Reflections client_class = new Reflections("java.net.http.HttpClient", false);
			Object o = client_class.invoke("newBuilder");
			o = Reflections.encapsulate(o).invoke("executor", new Class<?>[]
				{ Executor.class }, com.nulldev.util.web.HttpClient.HttpClient.gld.executor());
			o = Reflections.encapsulate(o).invoke("cookieHandler", new Class<?>[]
				{ CookieHandler.class }, new CookieHandler() {

					public final Map<String, List<String>> COOKIE_LIST = new EmptyMap<String, List<String>>();

					@Override
					public void put(URI uri, Map<String, List<String>> responseHeaders) throws IOException {
					}

					@Override
					public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) throws IOException {
						return COOKIE_LIST;
					}
				});
			client = Reflections.encapsulate(o).invoke("build");
		} else
			client = null;
	}

	private final AtomicReference<Object> /* java.net.http.HttpRequest.Builder */ connection = new AtomicReference<Object>();
	private final static Reflections connection_c;
	static {
		if (JVM.version() >= 11f) {
			connection_c = new Reflections("java.net.http.HttpRequest$Builder", false);
		} else
			connection_c = null;
	}

	public NativeHttpRequest() {
		if (JVM.version() < 11f)
			throw new UnsupportedOperationException("Unsupported JRE! Minimum: 11.0");
		this.connection.set(new Reflections("java.net.http.HttpRequest", false).invoke("newBuilder")); // java.net.http.HttpRequest.newBuilder();
		this.invoke("setHeader", new Class<?>[]
			{ String.class, String.class }, "User-Agent", com.nulldev.util.web.HttpClient.HttpClient.USER_AGENT);
		this.invoke("setHeader", new Class<?>[]
			{ String.class, String.class }, "Accept-Encoding", HttpStreamUtil.getSupportedCompressionString());
	}

	@SuppressWarnings("all")
	private Object invoke(final String o, final Object... v) {
		try {
			return connection_c.getClazz().getMethod(o).invoke(this.connection, v);
		} catch (Throwable e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("all")
	private Object invoke(final String o, final Class<?>[] clazz, final Object... v) {
		try {
			return connection_c.getClazz().getMethod(o, clazz).invoke(this.connection, v);
		} catch (Throwable e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public HttpRequest<T> url(String url) throws Exception {
		this.connection.set(invoke("uri", new Class<?>[]
			{ URI.class }, new URI(url)));
		return this;
	}

	@Override
	public HttpRequest<T> url(final URL url) throws Exception {
		this.connection.set(invoke("uri", new Class<?>[]
			{ URI.class }, url.toURI()));
		return this;
	}

	@Override
	public HttpRequest<T> header(String key, String value) {
		if (key == null || value == null || key.isEmpty() || value.isEmpty() || key.toLowerCase().equals("content-length")) {
			return this;
		}
		try {
			this.connection.set(invoke("setHeader", new Class<?>[]
				{ String.class, String.class }, key, value));
		} catch (Exception e) {
		}
		return this;
	}

	@Override
	public HttpRequest<T> header(final Entry entry) {
		if (entry == null || !entry.isValid() || entry.getKey().toLowerCase().equals("content-length")) {
			return this;
		}
		this.connection.set(invoke("setHeader", new Class<?>[]
			{ String.class, String.class }, entry.getKey(), entry.getValue()));
		return this;
	}

	@Override
	public HttpRequest<T> forceHeader(String key, String value) {
		return this.header(key, value);
	}

	@Override
	public HttpRequest<T> headers(final Entry[] entries) {
		if (entries == null || entries.length == 0) {
			return this;
		}
		for (final Entry entry : entries) {
			if (entry == null || !entry.isValid() || entry.getKey().toLowerCase().equals("content-length")) {
				continue;
			}
			try {
				this.connection.set(invoke("setHeader", new Class<?>[]
					{ String.class, String.class }, entry.getKey(), entry.getValue()));
			} catch (Exception e) {
			}
		}
		return this;
	}

	@Override
	public HttpRequest<T> timeout(Duration duration) {
		if (duration == null)
			return this;
		this.connection.set(invoke("setHeader", new Class<?>[]
			{ Duration.class }, duration));
		return this;
	}

	@Override
	public HttpResponse<T> create(final HttpMethod method, final Object body) throws Exception {
		if (body == null || method == HttpMethod.GET) {
			this.connection.set(invoke("GET"));
		} else {
			Object bp = null;
			if (body instanceof String) {
				bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofString", new Class<?>[]
					{ String.class }, (String) body);
			} else if (body instanceof InputStream) {
				bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofInputStream", new Supplier<InputStream>() {

					@Override
					public InputStream get() {
						return (InputStream) body;
					}
				});
			} else if (body instanceof byte[]) {
				bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofByteArray", (byte[]) body);
			} else if (body instanceof GeneratableContent) {
				bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofString", new Class<?>[]
					{ String.class }, ((GeneratableContent) body).makeAndClear());
			} else {
				throw new UnsupportedOperationException("Unable to create BodyPublisher from: " + body);
			}
			if (method == HttpMethod.OPTIONS) {
				throw new UnsupportedOperationException("OPTIONS is not supported.");
			} else if (method == HttpMethod.PUT) {
				this.connection.set(invoke("PUT", new Class<?>[]
					{ new Reflections("java.net.http.HttpRequest.BodyPublisher", false).getClazz() }, bp));
			} else if (method == HttpMethod.DELETE) {
				// this.connection = this.connection.DELETE(); temporary fix?
				this.connection.set(invoke("DELETE", new Class<?>[]
					{ new Reflections("java.net.http.HttpRequest.BodyPublisher", false).getClazz() }, bp));
			} else if (method == HttpMethod.POST) {
				this.connection.set(invoke("POST", new Class<?>[]
					{ new Reflections("java.net.http.HttpRequest.BodyPublisher", false).getClazz() }, bp));
			} else {
				throw new UnsupportedOperationException("Unknown method: " + method);
			}
		}
		final ResponseFormat format = this.getFormat();
		if (format == ResponseFormat.STREAM || format == ResponseFormat.STRING_VIA_STREAM || format == ResponseFormat.READER) {
			this.header("Accept-Encoding", "gzip, deflate");
		}
		final Object r = invoke("build");
		Object o = null;
		if (format == ResponseFormat.STRING) {
			o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("ofString");
		} else if (format == ResponseFormat.BYTES) {
			o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("ofByteArray");
		} else {
			o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("ofInputStream");
		}
		if (o != null && this.bufferSize > 0) {
			o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("buffering", o, this.bufferSize);
		}
		final Object x = ClassCache.forName("java.net.http.HttpClient").getMethod("send", new Class<?>[]
			{ new Reflections("java.net.http.HttpRequest", false).getClazz(), new Reflections("java.net.http.HttpResponse$BodyHandler", false).getClazz() })
				.invoke(client, r, o);
		if (body != null && body instanceof InputStream)
			IOUtils.closeQuietly((InputStream) body);
		return new NativeHttpResponse<T>(x, format);
	}

	@Override
	public Executable<HttpResponse<T>> createTask(HttpMethod method, Object body) throws Exception {
		final ControllableFutureTask<HttpResponse<T>> rr = new ControllableFutureTask<HttpResponse<T>>();
		try {
			if (body == null || method == HttpMethod.GET) {
				this.connection.set(invoke("GET"));
			} else {
				Object bp = null;
				if (body instanceof String) {
					bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofString", new Class<?>[]
						{ String.class }, (String) body);
				} else if (body instanceof InputStream) {
					bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofInputStream", new Supplier<InputStream>() {

						@Override
						public InputStream get() {
							return (InputStream) body;
						}
					});
				} else if (body instanceof byte[]) {
					bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofByteArray", (byte[]) body);
				} else if (body instanceof GeneratableContent) {
					bp = new Reflections("java.net.http.HttpRequest$BodyPublishers", false).invoke("ofString", new Class<?>[]
						{ String.class }, ((GeneratableContent) body).makeAndClear());
				} else {
					throw new UnsupportedOperationException("Unable to create BodyPublisher from: " + body);
				}
				if (method == HttpMethod.OPTIONS) {
					throw new UnsupportedOperationException("OPTIONS is not supported.");
				} else if (method == HttpMethod.PUT) {
					this.connection.set(invoke("PUT", new Class<?>[]
						{ new Reflections("java.net.http.HttpRequest.BodyPublisher", false).getClazz() }, bp));
				} else if (method == HttpMethod.DELETE) {
					this.connection.set(invoke("DELETE", new Class<?>[]
						{ new Reflections("java.net.http.HttpRequest.BodyPublisher", false).getClazz() }, bp));
				} else if (method == HttpMethod.POST) {
					this.connection.set(invoke("POST", new Class<?>[]
						{ new Reflections("java.net.http.HttpRequest.BodyPublisher", false).getClazz() }, bp));
				} else {
					throw new UnsupportedOperationException("Unknown method: " + method);
				}
			}
			final ResponseFormat format = this.getFormat();
			if (format == ResponseFormat.STREAM || format == ResponseFormat.STRING_VIA_STREAM || format == ResponseFormat.READER) {
				this.header("Accept-Encoding", "gzip, deflate");
			}
			final Object r = invoke("build");
			Object o = null;
			if (format == ResponseFormat.STRING) {
				o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("ofString");
			} else if (format == ResponseFormat.BYTES) {
				o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("ofByteArray");
			} else {
				o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("ofInputStream");
			}
			if (o != null && this.bufferSize > 0) {
				o = new Reflections("java.net.http.HttpResponse$BodyHandlers", false).invoke("buffering", o, this.bufferSize);
			}
			final Object x = ClassCache.forName("java.net.http.HttpClient").getMethod("send", new Class<?>[]
				{ new Reflections("java.net.http.HttpRequest", false).getClazz(), new Reflections("java.net.http.HttpResponse$BodyHandler", false).getClazz() })
					.invoke(client, r, o);
			if (body != null && body instanceof InputStream)
				IOUtils.closeQuietly((InputStream) body);
			if (!rr.complete(new NativeHttpResponse<T>(x, format))) {
				IOUtils.closeQuietly((Closeable) x);
			}
		} catch (Exception e) {
			rr.fail(e);
		}
		return rr;
	}

	@Override
	public Executable<HttpResponse<T>> createTaskAndRun(HttpMethod method, Object body) throws Exception {
		return this.createTask(method, body);
	}

	@Override
	public boolean isSupportedMethod(HttpMethod method) {
		return method == HttpMethod.GET || method == HttpMethod.POST || method == HttpMethod.DELETE || method == HttpMethod.PUT;
	}

	@Override
	public abstract ResponseFormat getFormat();

	private volatile int bufferSize = -1;

	@Override
	public HttpRequest<T> enableBuffering(int bufferSize) {
		if (bufferSize <= 0)
			return this;
		this.bufferSize = bufferSize;
		return this;
	}

	/* Read API */

	@Override
	public String url() {
		throw new UnsupportedOperationException();
	}

	@Override
	public Map<String, List<String>> headers() {
		throw new UnsupportedOperationException();
	}
}
